package edu.uw.cs.cse461.sp12.OS;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.json.JSONException;
import org.json.JSONObject;

import edu.uw.cs.cse461.sp12.OS.HTTPDService.HTTPProvider;
import edu.uw.cs.cse461.sp12.OS.exceptions.*;

public class DDNSResolverService extends RPCCallable implements HTTPProvider {
	public static final String ROOT_HOST = "cse461.cs.washington.edu";
	public static final int ROOT_PORT = 46130;
	public static final long CACHE_TIMEOUT = 30;	// in seconds
	private String myIP;
	private String myPassword;
	private int rpcPort;
	private Map<String, JSONObject> cache;
	private Map<String, ReRegisterThread> regThreads;
	
	public DDNSResolverService() throws DDNSNoSuchNameException, DDNSNoAddressException, DDNSRuntimeException, DDNSAuthorizationException, DDNSTTLExpiredException, DDNSZoneException {
		cache = new HashMap<String, JSONObject>();
		Properties p = OS.config();
		myPassword = p.getProperty("host.password");
		System.out.println("password" + myPassword);
		
		regThreads = new HashMap<String,ReRegisterThread>();
		
		// Register local RPC service
		try {
			myIP = ((RPCService)OS.getRPCService("rpc")).localIP();
			rpcPort = ((RPCService)OS.getRPCService("rpc")).localPort();
			
			String hostname = p.getProperty("host.name");
			register(hostname, rpcPort);
		} catch (UnknownHostException e) {
			System.out.println("Unknown Host Exception");
			e.printStackTrace();
		}
		
		// Example record for grading
		//DDNSRRecord funzone = resolve("funzone.cse461.");
		register("a.funzone.cse461.", rpcPort);
	}
	
	public String servicename() {
		return "ddnsresolver";
	}

	/*
	 * Client-accessible method to resolve a hostname to a DDNSRRecord
	 */
	public DDNSRRecord resolve(String target) throws DDNSNoAddressException, DDNSAuthorizationException, 
		DDNSNoSuchNameException, DDNSRuntimeException, DDNSTTLExpiredException, DDNSZoneException {
		try {
			
			// Look in cache
			if (cache.containsKey(target) && cache.get(target).getLong("expires") > System.currentTimeMillis()) {
				JSONObject response = cache.get(target);
				if (response.getString("resulttype").equals("resolverresult")) {
					JSONObject node = response.getJSONObject("node");
					DDNSRRecord result = new DDNSRRecord(node.getString("name"), node.getString("type"), 
							node.getString("ip"), node.getInt("port"));
					return result;
				} else if (response.getString("resulttype").equals("ddnsexception")) {
					throwException(response);
				}
			}
			
			JSONObject args = new JSONObject();
			args.put("name", target);
			JSONObject response = ddnsRPC(args, "resolve");
			
			// Update or add to cache
			response.put("expires", System.currentTimeMillis() + CACHE_TIMEOUT*1000);
			cache.put(target, response);
			
			// Check for exception and throw to client
			if(response.getString("resulttype").equals("ddnsexception")) {
				throwException(response);
			}
			
			// Create DDNSRRecord and return
			JSONObject node = response.getJSONObject("node");
			DDNSRRecord result = new DDNSRRecord(node.getString("name"), node.getString("type"), 
					node.getString("ip"), node.getInt("port"));
			return result;
			
		} catch (IOException e) {
			System.out.println("IO Exception: " + e.getMessage());
			shutdown();
			//e.printStackTrace();
		} catch (JSONException e) {
			System.out.println("JSON Exception: " + e.getMessage());
			e.printStackTrace();
		}
		return null;
	}
	

	
	/*
	 * Client-accessible method to register an app on DDNS
	 */
	public boolean register(String name, int port) throws DDNSNoSuchNameException, 
		DDNSNoAddressException, DDNSRuntimeException, DDNSAuthorizationException, 
		DDNSTTLExpiredException, DDNSZoneException {
		try {
			
			JSONObject args = new JSONObject();
			args.put("name", name);
			args.put("ip", myIP);
			args.put("port", port);
			args.put("password", myPassword);
			JSONObject response = ddnsRPC(args, "register");
			
			// Check for exception and throw to client
			if(response.getString("resulttype").equals("ddnsexception")) {
				throwException(response);
			}
			
			int lifetime = response.getInt("lifetime");
			
			// Create automatic reregistering thread
			ReRegisterThread reRegThread = new ReRegisterThread(name, myIP, port, lifetime);
			reRegThread.start();
			regThreads.put(name, reRegThread);
			return true;
		} catch (IOException e) {
			System.out.println("IO Exception: " + e.getMessage());
			shutdown();
			//e.printStackTrace();
		} catch (JSONException e) {
			System.out.println("JSON Exception: " + e.getMessage());
			e.printStackTrace();
		}
		return false;
	}
	
	/*
	 * Unregisters a name from DDNS (client-accessible)
	 */
	public boolean unregister(String name) throws DDNSNoAddressException, DDNSAuthorizationException, 
		DDNSNoSuchNameException, DDNSRuntimeException, DDNSTTLExpiredException, DDNSZoneException {
		try {
			
			JSONObject args = new JSONObject();
			args.put("name", name);
			args.put("password", myPassword);
			JSONObject response = ddnsRPC(args, "unregister");
			
			// Check for exception and throw to client
			if(response.getString("resulttype").equals("ddnsexception")) {
				throwException(response);
			}
			
		} catch (IOException e) {
			System.out.println("IO Exception: " + e.getMessage());
			shutdown();
			//e.printStackTrace();
		} catch (JSONException e) {
			e.printStackTrace();
		}
		return false;
	}
	
	/*
	 * Helper method to make DDNS RPC calls to name servers
	 */
	private JSONObject ddnsRPC(JSONObject args, String command) throws IOException, JSONException {
		System.out.println("sending " + command + " " + args);
		
		// Ask root name server
		InetAddress rootInet = InetAddress.getByName(ROOT_HOST);
		RPCCallerSocket socket = new RPCCallerSocket(ROOT_HOST, rootInet.getHostAddress(), Integer.toString(ROOT_PORT));
		JSONObject response = socket.invoke("ddns", command, args);
		socket.close();
		System.out.println(response.toString());
		// Iterate through name servers until matched
		while (!response.getString("resulttype").equals("ddnsexception") && !response.getBoolean("done")) {
			JSONObject node = response.getJSONObject("node");
			String rpcHostname = null;
			String rpcIP = null;
			String rpcPort = null;
			
			if (node.getString("type").equals("CNAME")) {
				// ask root to resolve alias
				args.put("name", node.getString("alias"));
				rpcHostname = ROOT_HOST;
				rpcIP = rootInet.getHostAddress();
				rpcPort = Integer.toString(ROOT_PORT);
			} else if (node.getString("type").equals("NS")) { 
				// ask next name server record to resolve target
				rpcHostname = node.getString("name");
				rpcIP = node.getString("ip");
				rpcPort = Integer.toString(node.getInt("port"));
			} else {
				System.out.println("Unexpected response: " + response);
			}
			
			socket = new RPCCallerSocket(rpcHostname, rpcIP, rpcPort); 
			response = socket.invoke("ddns", command, args);
			socket.close();
		}
		
		return response;
	}
	
	/*
	 * Helper to read exception response and throw appropriate exception
	 */
	private void throwException(JSONObject response) throws DDNSNoAddressException, 
		DDNSAuthorizationException, JSONException, DDNSNoSuchNameException, 
		DDNSRuntimeException, DDNSTTLExpiredException, DDNSZoneException {
		String name = response.getString("name");
		String message = response.getString("message");
		switch (response.getInt("exceptionnum")) {
		case 1:
			throw new DDNSNoSuchNameException(name + ": " + message);
		case 2:
			throw new DDNSNoAddressException(name + ": " + message);
		case 3:
			throw new DDNSAuthorizationException(name + ": " + message);
		case 4:
			throw new DDNSRuntimeException(message);
		case 5:
			throw new DDNSTTLExpiredException(name + ": " + message);
		case 6:
			throw new DDNSZoneException(name + ": " + message);
		default:
			//throw new Exception("Unknown DDNSException number");
		}
	}
	
	
	public void shutdown() {
		
		Set<String> names = regThreads.keySet();
		
		// Interrupt threads
		for (String name : names) {
			regThreads.get(name).interrupt();
		}
		
		// Unregister
		for (String name : names) {
			try {
				unregister(name);
			} catch (Exception e) {
				System.out.println("Exception unregistering on shutdown: " + e.getMessage());
			}
		}
		
		regThreads.clear();
	}

	/*
	 * Thread which registers a name, sleeps for "lifetime", repeats until interrupted
	 */
	private class ReRegisterThread extends Thread {
		private String name;
		private String ip;
		private int port;
		private int lifetime;
		
		public ReRegisterThread(String name, String ip, int port, int lifetime) throws UnknownHostException {
			this.name = name;
			this.ip = ip;
			this.port = port;
			this.lifetime = lifetime;
		}
		
		@Override
		public void run() {
			while (!Thread.currentThread().isInterrupted()) {
				try {
					JSONObject args = new JSONObject();
					args.put("name", name);
					args.put("ip", ip);
					args.put("port", port);
					args.put("password", myPassword);
					JSONObject response = ddnsRPC(args, "register");
					
					// Check for exception and print message, wait 100 seconds before retry
					
					if(response.getString("resulttype").equals("ddnsexception")) {
						System.out.println("Exception in RPC DDNS register");
						//lifetime = 100;
					} else {
						lifetime = response.getInt("lifetime");
					}
					
				} catch (IOException e) {
					System.out.println("IO Exception: " + e.getMessage());
					//lifetime = ;
					//e.printStackTrace();
				} catch (JSONException e) {
					System.out.println("JSON Exception: " + e.getMessage());
					e.printStackTrace();
				}
				
				// Wait for (lifetime - 5) or 5 seconds
				int waitTime = lifetime / 3;
				//TODO change waittime to include a check for which server has the shortest timeout time. IE if the root server has a timeout
				//of 30 and our NS has a timeout of 100, it should re-register every 20 seconds and not every 90 or something like that
				try {
					System.out.println(waitTime);
					Thread.sleep(waitTime * 1000);
				} catch (InterruptedException e) {
					return;
				}
			}
			
		}
		
		@Override
		public String toString() {
			return name + " @ " + ip + ":" + port + ", " + lifetime;
		}
		
	}
	
	/**
     * Serves web pages.  The 0th element of uriArray is always null.
     * The next element names this service ("ddnsresolver").  The optional third
     * component is a name to be resolved before dumping the cache.
     */
    @Override
    public String httpServe(String[] uriArray) {
    	StringBuilder sb = new StringBuilder();
    	sb.append("Fun Zone DDNS Resolver\n\n");
    	sb.append("Current Reregister Threads: \n");
    	for (String key : regThreads.keySet()) {
    		ReRegisterThread thread = regThreads.get(key);
    		sb.append("\t" + thread + ", status: " + thread.getState() + "\n");
    	}
    	sb.append("\n");
    	sb.append("Current Cache Entries: \n");
    	for (String key : cache.keySet()) {
    		sb.append("\t" + key + "\n\t\t"+ cache.get(key) + "\n");
    	}
    	return sb.toString();
    }
}
